/**
 * jsgig.entity - JSGIG basic entity types and related classes.
 */
if (!jsgig) {
  throw new Error("jsgig/jsgig.js must be included before jsgig/entity.js.");
}
if (!jsgig.entity) { jsgig.entity = (function() {
var my = {};

/**
* Abstract base class for all other entities.
* 
* All subclasses of this class must define at least the following methods:
* 
*   getWidth() // returns a number
*   getHeight() // returns a number
*   update() // no return value
*   draw(dest, pos) // no return value
*   erase() // no return value
* 
* Subclasses that support collision should define the following methods:
* 
*   collide(other[, offset]) // returns another entity or null
*/
my.AbstractEntity = util.extend(
  Object,
  
  function AbstractEntity (pos) {
    // Prevent instantiation
    if (this.constructor === my.AbstractEntity)
      throw new TypeError("AbstractEntity cannot be instantiated.");
    this._parent = null;
    this._groups = [];
    this._x = 0;
    this._y = 0;
    this._rx = 0;
    this._ry = 0;
    this._active = true;
    this._visible = true;
    if (pos) {
      this.setPos(pos);
    }
  },
  
  function getParent() { return this._parent; },
  
  function setParentInternal(parent) {
    this._parent = parent;
  },
  
  function attach(parent) {
    if (!parent)
      jsgig.getSpace().add(this)
    else
      parent.attach(this);
  },
  
  function detach() {
    if (this.getParent())
      this.getParent().remove(this);
  },
  
  function getGroups() {
    return this._groups.slice();
  },
  
  function addGroupInternal(group) {
    var groups = this._groups;
    if (groups.indexOf(group) == -1)
      groups.push(group);
  },
  
  function removeGroupInternal(group) {
    var groups = this._groups;
    var groupInd = groups.indexOf(group);
    if (groupInd != -1)
      groups.splice(groupInd, 1);
  },
  
  function getActive() { return this._active; },
  
  function setActive(a) { return this._active = (a ? true : false); },
  
  function getVisible() { return this._visible; },
  
  function setVisible(v) { return this._visible = (v ? true : false); },
  
  function getSolid() { return this._solid; },
  
  function setSolid(s) { return this._solid = (s ? true : false); },
    
  function getX() { return this._x },
  function setX(x) { this._x = x; },
  function getY() { return this._y },
  function setY(y) { this._y = y; },
  function getPos() { return [this.getX(), this.getY()]; },
  function setPos(pos) { this._x = pos[0]; this._y = pos[1]; },
  
  function getSize() { return [this.getWidth(), this.getHeight()]; },
    
  function getRX() { return this._rx; },
  function setRX(rx) { this._rx = rx; },
  function getRY() { return this._ry; },
  function setRY(ry) { this._ry = ry; },
  function getRPos() { return [this.getRX(), this.getRY()]; },
  function setRPos(aPos) { this.setRX(apos[0]); this.setRY(apos[1]); },
    
  function getRect() {
    return jsgig.Rect(
      this.getX() + this.getRX(),
      this.getY() + this.getRY(),
      this.getWidth(),
      this.getHeight()
    );
  },
    
  function getAbsX() {
    var ax = this.getX();
    var parent = this.getParent();
    while (parent != null) {
      ax -= parent.getX();
      parent = parent.getParent();
    }
    return ax;
  },
  
  function setAbsX(ax) {
    var parent = this.getParent();
    while (parent != null) {
      ax += parent.getX();
      parent = parent.getParent();
    }
    this.setX(ax);
  },
  
  function getAbsY() {
    var ay = this.getY();
    var parent = this.getParent();
    while (parent != null) {
      ay -= parent.getY();
      parent = parent.getParent();
    }
    return ay;
  },
  
  function setAbsY(ay) {
    var parent = this.getParent();
    while (parent != null) {
      ay += parent.getY();
      parent = parent.getParent();
    }
    this.setY(ay);
  },
  
  function getAbsPos() {
    var ax = this.getX();
    var ay = this.getY();
    var parent = this.getParent();
    while (parent != null) {
      ax -= parent.getX();
      ay -= parent.getY();
      parent = parent.getParent();
    }
    return ay;
  },
  
  function setAbsPos(pos) {
    var ax = pos[0];
    var ay = pos[1];
    var parent = this.getParent();
    while (parent != null) {
      ax += parent.getX();
      ay += parent.getY();
      parent = parent.getParent();
    }
    this.setX(ax);
    this.setY(ay);
  },
  
  function getAbsRect() {
    var aPos = this.getAbsPos();
    apos[0] -= this.getRX();
    apos[1] -= this.getRY();
    return jsgig.Rect.fromArrays(apos, this.getSize());
  },
  
  function getRX() { return this._rx; },
  function getRY() { return this._ry; },
  function getRPos() { return [this.getRX(), this.getRY()]; }
);

/**
* The "default" entity type.
* 
* This extends AbstractEntity with the following capabilities:
* 
* - Size and registration which can each be set directly using setter methods,
* - An `image` attribute that is used to draw the entity as an HTML element.
*   Note that this "image" is not an HTML element itself (such as the those
*   produced by the HTMLImageElement and Image functions) but is, rather, a 
*   facilitating object that is used to create the HTML element or elements
*   needed and to place them into the DOM at the correct location. These
*   facilitating objects are created via the classes defined in the
*   `jsgig.image` module.
*/
my.Entity = util.extend(
  my.AbstractEntity,
  
  function Entity(pos, size) {
    my.AbstractEntity.call(this, pos);
    this._width = 0;
    this._height = 0;
    this._image = null;
    this._prevImage = null;
    if (size)
      this.setSize(size);
  },
  
  function getWidth() { return this._width; },
  function setWidth(w) { this._width = w },
  function getHeight() { return this._height; },
  function setHeight(h) { this._height = h },
  function setSize(size) { this.setWidth(size[0]); this.setHeight(size[1]); },
  
  function getImage() { return this._image; },
  function setImage(image) {
    this._prevImage = this._image;
    this._image = image;
    return image;
  },
  
  function draw(dest, pos) {
    if (this._image !== this._prevImage) {
      if (this._prevImage)
        this._prevImage.erase();
      this._prevImage = this._image;
    }
    if (this._image)
      this._image.draw(dest, pos);
  },
  
  function update() {
    var img = this.getImage();
    if (img)
      img.update();
  }
);

/**
* Used to contain entities in a many-to-many relationship.
* 
* Groups are not entities, but they can contain entities. Furthermore, each
* entity can be a member of more than one group. (Constrast this with
* containers, for which each entity may be a member of only one.)
*/
my.Group = util.extend(
  Object,
  
  function Group() {
    if (this.__Group__) return; this.__Group__ = 1;
    this._children = [];
  },
  
  function length() {
    return this._children.length;
  },

  function contains (child) {
    return this._children.indexOf(child) != -1;
  },

  function indexOf (child) {
    return this._children.indexOf(child);
  },
  
  function each (func, context) {
    if (context === undefined)
      context = this;
    var children = this.getChildren();
    for (var i in children) {
      func.call(context, children[i], i);
    }
  },
  
  function getChild(i) {
    return this._children[i];
  },
  
  function getChildren() {
    return this._children.slice();
  },
  
  function add(child) {
    var children = this._children;
    var childInd = children.indexOf(child);
    if (childInd == -1) {
      children.push(child);
      child.addGroupInternal(this);
    }
  },
  
  function insert(i, child) {
    throw new Error("TODO: GroupMixin.insert");
  },
  
  function extend(children) {
    for (var i=0; i<children.length; i++) {
      this.add(children[i]);
    }
  },
  
  function remove(child) {
    this.removeInternal(child);
    child.removeGroupInternal(this);
  },
  
  function removeInternal(child) {
    if (this.contains(child)) {
      this._children.splice(this._children.indexOf(child), 1);
    }
  },
  
  function shift (child, i) {
    this.insert(this.indexOf(child)+i, child);
  },
  
  function toTop (child) {
    this.remove(child);
    this.add(child);
  },
  
  function toBottom (child) {
    this.remove(child);
    this.insert(0, child);
  }
);

/**
* Mixin that allows containment of entities in a one-to-many relatioship.
*
* This mixin will give inheriting classes the ability to contain entities, but
* will restrict the entities to being a member only of the most recent
* container.
* 
* The restriction is only applied to other containers, and not to other groups.
* For example, if an entity is added to group A and then group B, the entity
* will be a member of both groups. However, if the entity is a member of
* container C and then is added to container D, the entity will cease to be a
* member of container C and will only be a member of D. (However, it will still
* be a member of groups A and B regardless.)
*/
my.ContainerMixin = util.extend(
  my.Group,
  
  function ContainerMixin() {
    if (this.__ContainerMixin__) return; this.__ContainerMixin__ = 1;
    my.Group.call(this);
  },
  
  function add(child) {
    if (child.getParent() != null) {
      child.getParent().removeInternal(child);
    }
    this._children.push(child);
    child.setParentInternal(this);
  },
  
  function insert(i, child) {
    if (child.getParent() != null) {
      if (child.getParent() == this && this.indexOf(child) >= i) {
        i--;
      }
      child.getParent().removeInternal(child);
    }
    this._children.splice(i, 0, child);
  },
  
  function extend(children) {
    for (var i=0; i<children.length; i++) {
      this.add(children[i]);
    }
  },
  
  function remove(child) {
    this.removeInternal(child);
    child.setParentInternal(null);
  },
  
  function removeInternal(child) {
    if (!this.contains(child)) {
      throw "Child not present: " + child.toString();
    }
    this._children.splice(this._children.indexOf(child), 1);
  }
);

/**
* An entity that can contain other entities.
*
* The size and registration of a container is dependant on the rectangles of its
* contents. This works such that the left edge of the container will always be 
* the left edge of the rectangle of any contained entity whose left edge is 
* farthest left, and likewise for the other edges.
*
* Objects moving in the container will not cause the container's position to
* change, but they may affect size and registration and, therefore, the 
* container's rectangle. Moving the container (changing its x,y position) will 
* cause the absolute positions of all objects in the container to change, but 
* not their local positions.
*
* Basic containers are not represented by HTML elements. When drawn, the 
* container's entities are drawn directly onto the element to which the group 
* itself was drawn.
*/
my.Container = util.extend(
  [my.AbstractEntity, my.ContainerMixin],
  
  function Container (pos) {
    my.AbstractEntity.call(this, pos);
    my.ContainerMixin.call(this);
    this._toErase = [];
  },
  
  function add(child) {
    my.ContainerMixin.prototype.add.call(this, child);
    if (this._toErase.indexOf(child) != -1) {
      this._toErase.splice(this._toErase.indexOf(child), 1);
    }
  },
  
  function insert(i, child) {
    my.ContainerMixin.prototype.insert.call(this, i, child);
    if (this._toErase.indexOf(child) != -1) {
      this._toErase.splice(this._toErase.indexOf(child), 1);
    }
  },
  
  function removeInternal(child) {
    my.ContainerMixin.prototype.removeInternal.call(this, child);
    this._toErase.push(child);
  },
  
  function getRX () {
    if (this.length()) {
      var it = this.iter();
      var child = it.next();
      var childRect = child.getRect();
      var loLeft = childRect.getLeft();
      while (it.hasNext()) {
        child = it.next();
        childRect = child.getRect();
        if (childRect.getLeft() < loLeft) {
            loLeft = childRect.getLeft();
        }
      }
      return -loLeft;
    }
    else {
        return 0;
    }
  },
  
  function setRX (rx) {
    throw new Error("Cannot set registration for groups.");
  },
  
  function getRY () {
    if (this.length()) {
      var it = this.iter();
      var child = it.next();
      var childRect = child.getRect();
      var loTop = childRect.getTop();
      while (it.hasNext()) {
        child = it.next();
        childRect = child.getRect();
        if (childRect.getTop() < loTop) {
            loTop = childRect.getTop();
        }
      }
      return -hiBottom;
    }
    else {
        return 0;
    }
  },
  
  function setRY (ry) {
    throw new Error("Cannot set registration for groups.");
  },
  
  function getWidth () {
    if (this.length()) {
      var it = this.iter();
      var child = it.next();
      var childRect = child.getRect();
      var hiRight = childRect.getRight();
      var loLeft = childRect.getLeft();
      while (it.hasNext()) {
        child = it.next();
        childRect = child.getRect();
        if (childRect.getLeft() < loLeft)
          loLeft = childRect.getLeft();
        if (childRect.getRight() > hiRight)
          hiRight = childRect.getRight();
      }
      var w = hiRight - loLeft;
      return w;
    }
    else
      return 0;
  },
  
  function getHeight () {
    if (this.length()) {
      var it = this.iter();
      var child = it.next();
      var childRect = child.getRect();
      var hiBottom = childRect.getBottom();
      var loTop = childRect.getTop();
      while (it.hasNext()) {
        child = it.next();
        childRect = child.getRect();
        if (childRect.getTop() < loTop)
          loTop = childRect.getTop();
        if (childRect.getBottom() > hiBottom)
          hiBottom = childRect.getBottom();
      }
      var h = hiBottom - loTop;
      return h;
    }
    else
      return 0;
  },
  
  function draw(dest, pos) {
    this._toErase.map(_eraseChild);
    this._toErase.splice(0,this._toErase.length);
    var children = this.getChildren();
    for (var i in children) {
      var child = children[i];
      if (child.getVisible()) {
        var childPos = child.getPos();
        childPos[0] += pos[0]; // NOTE: Do NOT use the registration point,
        childPos[1] += pos[1]; // as it is calculated from positions of children
                               // and is therefore already accounted for.
        child.draw(dest, childPos);
      }
      else
        child.erase();
    }
  },
  
  function update() {
    this.each(_updateChild);
  }
);

function _updateChild(child) {
  if (child.getActive())
    child.update();
}

function _eraseChild(child) {
  child.erase();
}

/**
* The root group for all other elements.
*
* A Space is infinite in size (returns [Infinity, Infinity] for size, registered
* at 
*
* When the system is initalized via jsgig.init() a Space element is created and
* set to be the root element of the system. It will be drawn to the screen 
* element during the draw phase of the main game cycle.
*/
my.Space = util.extend(
  my.Container,
  
  function Space () {
    my.Container.call(this, [0,0]);
  },
  
  function setParentInternal (parent) {
    throw new TypeError("Space cannot have a parent.")
  },
  function getWidth () { return Infinity; },
  function getHeight () { return Infinity; },
  function getRX () { return Infinity; },
  function getRY () { return Infinity; },

  function update () {
    my.Container.prototype.update.call(this);
  }
);

// End of module `jsgig.entity`
return my; }) (); }